// ignore_for_file: sdk_version_async_exported_from_core
// ignore_for_file: unawaited_futures
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

// Real class
class Cat {
  String sound() => "Meow";
  bool eatFood(String food, {bool hungry}) => true;
  Future<void> chew() async => print("Chewing...");
  int walk(List<String> places) => 7;
  void sleep() {}
  void hunt(String place, String prey) {}
  int lives = 9;
}

// Mock class
class MockCat extends Mock implements Cat {}

// Fake class
class FakeCat extends Fake implements Cat {
  @override
  bool eatFood(String food, {bool hungry}) {
    print('Fake eat $food');
    return true;
  }
}

void main() {
  Cat cat;

  setUp(() {
    // Create mock object.
    cat = MockCat();
  });

  test("Let's verify some behaviour!", () {
    // Interact with the mock object.
    cat.sound();

    // Verify the interaction.
    verify(cat.sound());
  });

  test("How about some stubbing?", () {
    // Unstubbed methods return null.
    expect(cat.sound(), null);

    // Stub a method before interacting with it.
    when(cat.sound()).thenReturn("Purr");
    expect(cat.sound(), "Purr");

    // You can call it again.
    expect(cat.sound(), "Purr");

    // Let's change the stub.
    when(cat.sound()).thenReturn("Meow");
    expect(cat.sound(), "Meow");

    // You can stub getters.
    when(cat.lives).thenReturn(9);
    expect(cat.lives, 9);

    // You can stub a method to throw.
    when(cat.lives).thenThrow(RangeError('Boo'));
    expect(() => cat.lives, throwsRangeError);

    // We can calculate a response at call time.
    var responses = ["Purr", "Meow"];
    when(cat.sound()).thenAnswer((_) => responses.removeAt(0));
    expect(cat.sound(), "Purr");
    expect(cat.sound(), "Meow");
  });

  test("Argument matchers", () {
    // You can use plain arguments themselves
    when(cat.eatFood("fish")).thenReturn(true);

    // ... including collections
    when(cat.walk(["roof", "tree"])).thenReturn(2);

    // ... or matchers
    when(cat.eatFood(argThat(startsWith("dry")))).thenReturn(false);

    // ... or mix aguments with matchers
    when(cat.eatFood(argThat(startsWith("dry")), hungry: true))
        .thenReturn(true);
    expect(cat.eatFood("fish"), isTrue);
    expect(cat.walk(["roof", "tree"]), equals(2));
    expect(cat.eatFood("dry food"), isFalse);
    expect(cat.eatFood("dry food", hungry: true), isTrue);

    // You can also verify using an argument matcher.
    verify(cat.eatFood("fish"));
    verify(cat.walk(["roof", "tree"]));
    verify(cat.eatFood(argThat(contains("food"))));

    // You can verify setters.
    cat.lives = 9;
    verify(cat.lives = 9);

    cat.hunt("backyard", null);
    verify(cat.hunt("backyard", null)); // OK: no arg matchers.

    cat.hunt("backyard", null);
    verify(cat.hunt(argThat(contains("yard")),
        argThat(isNull))); // OK: null is wrapped in an arg matcher.
  });

  test("Named arguments", () {
    // GOOD: argument matchers include their names.
    when(cat.eatFood(any, hungry: anyNamed('hungry'))).thenReturn(true);
    when(cat.eatFood(any, hungry: argThat(isNotNull, named: 'hungry')))
        .thenReturn(false);
    when(cat.eatFood(any, hungry: captureAnyNamed('hungry'))).thenReturn(false);
    when(cat.eatFood(any, hungry: captureThat(isNotNull, named: 'hungry')))
        .thenReturn(true);
  });

  test("Verifying exact number of invocations / at least x / never", () {
    cat.sound();
    cat.sound();
    // Exact number of invocations
    verify(cat.sound()).called(2);

    cat.sound();
    cat.sound();
    cat.sound();
    // Or using matcher
    verify(cat.sound()).called(greaterThan(1));

    // Or never called
    verifyNever(cat.eatFood(any));
  });

  test("Verification in order", () {
    cat.eatFood("Milk");
    cat.sound();
    cat.eatFood("Fish");
    verifyInOrder([cat.eatFood("Milk"), cat.sound(), cat.eatFood("Fish")]);
  });

  test("Making sure interaction(s) never happened on mock", () {
    verifyZeroInteractions(cat);
  });

  test("Finding redundant invocations", () {
    cat.sound();
    verify(cat.sound());
    verifyNoMoreInteractions(cat);
  });

  test("Capturing arguments for further assertions", () {
    // Simple capture:
    cat.eatFood("Fish");
    expect(verify(cat.eatFood(captureAny)).captured.single, "Fish");

    // Capture multiple calls:
    cat.eatFood("Milk");
    cat.eatFood("Fish");
    expect(verify(cat.eatFood(captureAny)).captured, ["Milk", "Fish"]);

    // Conditional capture:
    cat.eatFood("Milk");
    cat.eatFood("Fish");
    expect(
        verify(cat.eatFood(captureThat(startsWith("F")))).captured, ["Fish"]);
  });

  test("Waiting for an interaction", () async {
    Future<void> chewHelper(Cat cat) {
      return cat.chew();
    }

    // Waiting for a call.
    chewHelper(cat);
    await untilCalled(cat.chew()); // This completes when cat.chew() is called.

    // Waiting for a call that has already happened.
    cat.eatFood("Fish");
    await untilCalled(cat.eatFood(any)); // This completes immediately.
  });

  test("Fake class", () {
    // Create a new fake Cat at runtime.
    var cat = FakeCat();

    cat.eatFood("Milk"); // Prints 'Fake eat Milk'.
    expect(() => cat.sleep(), throwsUnimplementedError);
  });
}
